<?php
// @formatter:off
/**
 * @file WCDirectory.php
 * @author Alejandro Dario Simi
 * @date $Date: 2013-06-16 19:41:09 +0000 (Sun, 16 Jun 2013) $
 *
 * $Id: WCDirectory.php 72 2013-06-16 19:41:09Z daemonraco@gmail.com $
 * $URL: http://wcomix.googlecode.com/svn/tags/wcomix-1.0-BETA1/includes/representations/WCDirectory.php $
 */
// @formatter:on

/**
 * @class WCDirectory
 * This class represents entries saved in the table '%directories'.
 */
class WCDirectory extends WCGTableRepresentation implements WCPreviews {
	/**
	 * @var string Special character to use as separator in column
	 * dir_previews.
	 */
	public static $PreviewsSeparator = "|";
	/**
	 * @var int This constant holds the maximum length of column
	 * dir_previews.
	 */
	public static $PreviewsMaxLength = 640;
	/**
	 * @var string[] This variable holds the list of columns that cannot be
	 * directly modified by the pseudo-method __set().
	 * In this case dir_id, dir_group and dir_name.
	 */
	protected $_PropertiesReadOnly = array(
		"id",
		"group",
		"name"
	);
	/**
	 * @var string[] This variable holds the list of columns that must be
	 * treated as booleans and then send to the database as 'Y' and 'N' by
	 * the pseudo-method __set().
	 * In this case col_valid_preview and col_indexed.
	 */
	protected $_PropertiesBoolean = array(
		"valid_preview",
		"indexed",
		"lost"
	);
	/**
	 * @var string This is the prefix of every column on the table
	 * '%directories'.
	 */
	protected $_PropertiesPrefix = "dir_";
	/**
	 * @var string This is the name of the represented table. In this case,
	 * '%directories'.
	 */
	protected $_PropertiesTable = "directories";
	/**
	 * @var string This variable holds the full path to group's root
	 * directory. It's the same than method 'WCGroup::root()'.
	 */
	protected $_root;
	/**
	 * @var WCGContainer This container holds all comics that are physically
	 * present in this directory.
	 */
	protected $_comics;
	/**
	 * @var WCGContainer This container holds all subdirectories that, based
	 * on their paths, are supposed to be physically in this directory.
	 */
	protected $_directories;
	/**
	 * Class basic destructor.
	 */
	public function __destruct() {
		//
		// This class has nothing to clean before deleting.
	}
	//
	// Public methods.
	/**
	 * This method allows to get a directory base-name.
	 *
	 * @return string Returns a directory name.
	 */
	public function basename() {
		return basename($this->name);
	}
	/**
	 * This method returns the full path of the directory.
	 *
	 * @return string Returns a system full path.
	 */
	public function fullPath() {
		return $this->_group->root.$this->name;
	}
	/**
	 * This method creates a new random previews list based on directory's
	 * comics, and sets it in column dir_previews.
	 *
	 * @return boolean Returns true if a previews list was generated.
	 */
	public function generatePreviews() {
		//
		// This is the value to return by default.
		$out = false;
		//
		// Loading global varibles.
		global $wcPageCache;
		global $wcDefaults;
		//
		// Invalidating any cache held on this directory.
		$wcPageCache->invalidateByDirectory($this);
		//
		// Creating an empty list to hold all found previews.
		$previews = array();
		//
		// Loading the lists of comics and counting the amount of items
		// to check.
		// @{
		$comics = $this->membersFull();
		$comicCount = count($comics);
		// @}
		//
		// Because it is already counted, it is a good opportunity to keep
		// directory's stats updated.
		$this->count = $comicCount;
		//
		// If there are comics, it adds their previews to the list.
		if($comicCount > 0) {
			//
			// Creating a limit of previews to set. Ten is enough.
			$comicCount = $comicCount > 10 ? 10 : $comicCount;
			//
			// If there are more than one, a '$comicCount' amount of
			// previews should be picked randomly from the full list.
			if($comicCount > 1) {
				//
				// Picking random previews.
				foreach(array_rand($comics,$comicCount) as $id) {
					$previews[] = $comics[$id]->previewUri();
				}
			} elseif($comicCount > 0) {
				//
				// Choosing the only preview.
				// @{
				$key = array_keys($comics);
				$key = array_shift($key);
				$previews[] = $comics[$key]->previewUri();
				// @}
			}
			//
			// Removing default previews.
			foreach($previews as $key => $value) {
				if($value == $wcDefaults['preview']) {
					unset($previews[$key]);
				}
			}
			//
			// Counting how many previews were picked.
			$count = count($previews);
			//
			// If there are previews and there are as much as the
			// limit it is possible to set the list of previews.
			if($count && $count == $comicCount) {
				//
				// Sticking all previews into a string and
				// assigning it to column dir_previews.
				$this->previews = implode(self::$PreviewsSeparator, $previews);
				//
				// Directory's previews are set as valid.
				$this->valid_preview = true;
				//
				// At this point, it is considered that a list of
				// previews was found and set to the current
				// directory.
				$out = true;
			}
		}
		//
		// Returning generation results.
		return $out;
	}
	/**
	 * This method checks if there is a preview for this directory.
	 *
	 * @return boolean Returns true when there's a preview for this
	 * directory.
	 */
	public function hasPreview() {
		//
		// It has the same result as hasPreviews().
		return $this->hasPreviews();
	}
	/**
	 * This method checks if there are previews for this directory.
	 *
	 * @return boolean Returns true when there are previews for this
	 * directory.
	 */
	public function hasPreviews() {
		//
		// When column dir_previews has contents, it means there are
		// previews.
		return $this->previews != "" ? true : false;
	}
	/**
	 * This method allows to know it the physical directory had modifications
	 * since the last check.
	 *
	 * @return boolean Returns true when a modification is detected.
	 */
	public function isChanged() {
		//
		// Setting the default value to be returned.
		$out = false;
		//
		// Checking if this directory has no issues.
		if($this->ok()) {
			//
			// Getting system information of the physical directory.
			$st = $this->stat();
			//
			// If any time property of the directory differ with
			// that saved in the database, it is considered as
			// modified.
			$out = $st["mtime"] != $this->mtime || $st["ctime"] != $this->ctime || $st["atime"] != $this->atime;
		}
		//
		// If a modification was detected and it's previews are
		// considered to be valid, this must change.
		if($out && $this->valid_preview) {
			$this->valid_preview = false;
		}
		//
		// Returning check's results.
		return $out;
	}
	/**
	 * This method is a re-implementation of its parent method to add some
	 * specific checks.
	 *
	 * @param int $id Directory id to load.
	 * @param WCGroup $group Group to which the directory is associated.
	 * @return boolean Returns true when it loads without issues.
	 */
	public function load($id, WCGroup &$group) {
		//
		// Calling for the inherited method.
		parent::load($id, $group);
		//
		// If it's ok, it means the parent loaded had no issues.
		if($this->ok()) {
			//
			// Setting root directory based on its group.
			$this->_root = $this->_group->root();
			//
			// Checking if the physical directory exists and if it
			// is readable.
			// @{
			$fullPath = $this->fullPath();
			$this->_ok = is_dir($fullPath) && is_readable($fullPath);
			// @}
			//
			// If there are no previews, this property is forced
			// to be false.
			$this->_info["dir_previews"] = $this->_info["dir_previews"] ? $this->_info["dir_previews"] : false;

			if($this->_comics !== null) {
				$this->_comics = null;
			}
			//
			// Ensuring containers existence.
			$this->createContainers();
		}
		//
		// Returning loading results.
		return $this->ok();
	}
	/**
	 * This method attempts to load the object based on a directory path. The
	 * path to search must be a full path from the root directory of the
	 * specified group.
	 *
	 * @param string $directory Full path to look for.
	 * @param WCGroup $group Group where the directory to look for belongs.
	 * @return boolean Returns true when it was successfully loaded.
	 */
	public function loadByPath(string $directory, WCGroup &$group) {
		//
		// Setting internal properties to their initial states.
		// @{}
		$this->_info = array();
		$this->_ok = false;
		// @}
		//
		// Preparing a statement to search in the database a directory
		// based on the requested path.
		// @{}
		$stmt = null;
		$stmtID = __CLASS__."::".__FUNCTION__;
		if($this->_db->has($stmtID)) {
			$stmt = $this->_db->get($stmtID);
		} else {
			$query = "select  *\n";
			$query .= "from    {$this->_dbprefix}directories\n";
			$query .= "where   dir_group = :group\n";
			$query .= " and    dir_name  = :name\n";

			$stmt = $this->_db->prepare($stmtID, $query);
		}
		// @}
		//
		// Asking for the path.
		// @{
		$params = array(
			":group" => $group->id,
			":name" => $directory
		);
		if($stmt->execute($params) && $stmt->rowCount() > 0) {
			//
			// At this point, a match was found and it should be
			// loaded using main loader method.
			$result = $stmt->fetch();
			$this->load($result["dir_id"], $group);
		}
		// @}
		//
		// Returning load status.
		return $this->ok();
	}
	/**
	 * This method allows to get the list of comic ids that belongs to this
	 * directory.
	 *
	 * @return int[] Returns a list of comics.
	 */
	public function members() {
		//
		// Loading members list
		$this->loadMembers();
		//
		// If there are no issues it returns the list, otherwise, it
		// returns false.
		return $this->ok() ? $this->_comics->ids() : false;
	}
	/**
	 * This method allows to get the list of comics that belongs to this
	 * directory.
	 *
	 * @return WCComics[] Returns a list of comics.
	 */
	public function membersFull() {
		//
		// Loading members list
		$this->loadMembers();
		//
		// If there are no issues on this object, the list of items of its
		// container is returned, otherwise, an empty array is returned.
		return $this->ok() ? $this->_comics->items() : array();
	}
	/**
	 * This method allows to get the parent directory for the current one.
	 *
	 * @return WCDirectory Parent directory.
	 */
	public function parentDirectory() {
		//
		// Loading global pointers.
		global $wcDirectoriesHolder;
		//
		// Setting the default value to returned.
		$out = null;
		//
		// Expanding directory names based on the current one.
		$parentDir = explode("/", $this->name);
		//
		// Removing the last item because it is the base-name for this
		// directory.
		array_pop($parentDir);
		//
		// If there's only one element, it is the root directory,
		// otherwise it is imploded.
		$parentDir = count($parentDir) == 1 ? "/" : implode("/", $parentDir);
		//
		// Looking for the built directory path.
		$directory = @$wcDirectoriesHolder->itemByName($parentDir, $this->_group);
		//
		// If it's found it's assigned to be returned. Otherwise it's
		// considered to have no parent directory configured on database.
		if($directory && $directory->ok()) {
			if($directory->id != $this->id) {
				$out = $directory;
			}
		}
		//
		// Returning found directory.
		return $out;
	}
	/**
	 * This method allows to get the thumbnail associated with the current
	 * directory.
	 *
	 * @return string Returns an image URIs.
	 */
	public function preview() {
		//
		// Checking if there are previews set.
		if($this->previews) {
			//
			// Getting the list of possible thumbnails.
			$list = $this->previews();
			//
			// Returning the first URI from the list.
			return array_shift($list);
		} else {
			//
			// Where there is no preview, it returns an empty string.
			return "";
		}
	}
	/**
	 * This method allows to get the list of thumbnails associated with the
	 * current directory.
	 *
	 * @return string[] Returns a list of image URIs.
	 */
	public function previews() {
		//
		// Checking if there are previews set.
		if($this->previews) {
			//
			// Splitting the contents of column col_previews and
			// returning them as an array.
			return explode(self::$PreviewsSeparator, $this->previews);
		} else {
			//
			// Where there are no previews, it returns an empty array.
			return array();
		}
	}
	/**
	 * This method allows to get the full path to this directory's group
	 * root. It's similar to method 'WCGroup::root()'.
	 *
	 * @return string Returns the full path of group's root directory.
	 */
	public function root() {
		return $this->_root;
	}
	/**
	 * This method performs the scheduled renaming operation.
	 *
	 * @return boolean Returns true when the operation was performed without
	 * issues. If the comic was not scheduled for renaming, it returns false.
	 */
	public function setNextName() {
		//
		// This is the default value to be returned.
		$ok = true;
		//
		// Generating full paths for origin and destination.
		$oldPath = WCSanitizer::SystemPath("{$this->root()}{$this->name}");
		$newPath = WCSanitizer::SystemPath("{$this->root()}{$this->next_name}");
		//
		// Making some preliminary checks:
		//	- If the directory wasn't scheduled, it is an error.
		//	- If the current full path isn't a directory, it is an
		//	  error.
		//	- If the destination full path is an existing file, it is
		//	  an error.
		$ok = $this->next_name != "" && is_dir($oldPath) && !file_exists($newPath);
		//
		// If everything is ok, the file is renamed.
		if($ok) {
			//
			// Renaming the directory.
			rename($oldPath, $newPath);
			//
			// Checking that the new directory exists and the old one
			// doesn't.
			$ok = !is_dir($oldPath) && is_dir($newPath);
		}
		//
		// If the directory was successfully renamed.
		if($ok) {
			//
			// Many properties of the directory had changed,
			// therefore, to avoid problems, it is reloaded.
			$ok = $this->load($this->id, $this->_group);
		}
		//
		// If it was successfully reloaded.
		if($ok) {
			$this->next_name = "";
		}
		//
		// Returning the process results.
		return $ok;
	}
	/**
	 * This method ask for the physical directory descriptor information.
	 *
	 * @return string[string] Returns the same value than function 'stat()'.
	 */
	public function stat() {
		return $this->ok() ? stat("{$this->root()}{$this->name}") : false;
	}
	/**
	 * This method looks for other directories that, based on their paths,
	 * are sub-directories of the current one.
	 *
	 * @return WCDirectory[] Returns a list of directories.
	 */
	public function subDirectories() {
		//
		// Checking that the list of sub-directories is not yet loaded.
		if($this->ok() && !$this->_directories->count()) {
			//
			// Clearing any previews load information.
			$this->_directories->reset();
			//
			// Preparing a statement to load the list of possible
			// sub-directoties.
			// @{
			$stmt = null;
			$stmtId = __CLASS__."::".__FUNCTION__;
			if($this->_db->has($stmtId)) {
				$stmt = $this->_db->get($stmtId);
			} else {
				$query = "select  dir_id,\n";
				$query .= "        dir_name\n";
				$query .= "from    {$this->_dbprefix}directories\n";
				$query .= "where   dir_group = :group\n";
				$query .= " and    dir_name  like concat(:name,'%')\n";
				$query .= "order by dir_name asc\n";

				$stmt = $this->_db->prepare($stmtId, $query);
			}
			// @}
			//
			// Asking for the list of sub-directories.
			// @{
			$params = array(
				":group" => $this->_group->id,
				":name" => $this->name
			);
			if($stmt->execute($params)) {
				//
				// Loading global pointers.
				global $wcDirectoriesHolder;
				//
				// Counting how many directory names are in this
				// directory's path.
				$jumps = count(explode("/", $this->name)) + ($this->name != "/" ? 1 : 0);
				//
				// Analyzing each database result.
				foreach($stmt->fetchAll() as $row) {
					//
					// If a found directory is equal to the
					// current one, it's not a sub-directory.
					if($row["dir_id"] == $this->id) {
						continue;
					}
					//
					// If the amount of names in a found path
					// is more than one name longer, it's
					// inside sub-directory and it's ignored.
					if(count(explode("/", $row["dir_name"])) != $jumps) {
						continue;
					}
					//
					// At this point, it is considered to be
					// a sub-directory, therefore, it's added
					// to the list.
					$this->_directories->addId($row["dir_id"]);
				}
			}
			// @}
		}
		//
		// Returning the list of subdirectories.
		return $this->_directories->items();
	}
	/**
	 * This method sets in the database entry of this directory the current
	 * values of the physical directory times (gotten with 'stat()').
	 */
	public function updateTimes() {
		//
		// Setting the default value to be returned.
		$out = false;
		//
		// This operation can only be run if there's no problem with
		// this object.
		if($this->ok()) {
			//
			// Loading global pointers.
			global $wcPageCache;
			//
			// Because the database entry is going to change, any
			// cache on this directory is no longer valid.
			$wcPageCache->invalidateByDirectory($this);
			//
			// Getting current data from the physical descriptor.
			$st = $this->stat();
			//
			// Creating a statement to update the database entry.
			// @{
			$stmt = null;
			$stmtId = __CLASS__."::".__FUNCTION__;
			if($this->_db->has($stmtId)) {
				$stmt = $this->_db->get($stmtId);
			} else {
				$query = "update  {$this->_dbprefix}directories\n";
				$query .= "set     dir_atime = :atime,\n";
				$query .= "        dir_mtime = :mtime,\n";
				$query .= "        dir_ctime = :ctime\n";
				$query .= "where   dir_group = :group\n";
				$query .= " and    dir_id    = :id\n";
				$stmt = $this->_db->prepare($stmtId, $query);
			}
			// @}
			//
			// Setting the updated data.
			// @{
			$params = array(
				":atime" => $st["atime"],
				":mtime" => $st["mtime"],
				":ctime" => $st["ctime"],
				":group" => $this->_group->id,
				":id" => $this->id
			);
			if($out = $stmt->execute($params)) {
				//
				// Reloading the object with the updated data.
				$out = $this->load($this->id, $this->_group);
				//
				// Forcing an update of previews.
				$this->generatePreviews();
			}
			// @}
		}
		//
		// Returning operation status.
		return $out;
	}
	//
	// Protected methods
	/**
	 * The method creates all required containers.
	 */
	protected function createContainers() {
		//
		// Checking comics container.
		if($this->_comics == null) {
			//
			// Loading global pointers.
			global $wcComicsHolder;
			//
			// Creating a list for comics.
			$this->_comics = new WCGContainer($wcComicsHolder, $this->_group);
		}
		//
		// Checking sub-directories container.
		if($this->_directories == null) {
			//
			// Loading global pointers.
			global $wcDirectoriesHolder;
			//
			// Creating a list for sub-directories.
			$this->_directories = new WCGContainer($wcDirectoriesHolder, $this->_group);
		}
	}
	/**
	 * This method loads the list of comics that belong to this directory.
	 * @note Sub-directories' comics are not considered as memebers.
	 */
	protected function loadMembers() {
		//
		// Ensuring containers existence.
		$this->createContainers();
		//
		// Checking that this directory is ok and the comic's list is
		// not loaded yet.
		if($this->ok() && !$this->_comics->count()) {
			//
			// Clearing any previews load information.
			$this->_comics->reset();
			//
			// Creating a statement to load comics.
			// @{
			$stmt = null;
			$stmtId = __CLASS__."::".__FUNCTION__;
			if($this->_db->has($stmtId)) {
				$stmt = $this->_db->get($stmtId);
			} else {
				$query = "select  cmc_id\n";
				$query .= "from    {$this->_dbprefix}comics\n";
				$query .= "where   cmc_group     = :group\n";
				$query .= " and    cmc_directory = :directory\n";
				$query .= " and    cmc_lost      = 'N'\n";
				$query .= "order by cmc_name asc\n";

				$stmt = $this->_db->prepare($stmtId, $query);
			}
			// @}
			//
			// Asking for comics.
			// @{
			$params = array(
				":group" => $this->_group->id,
				":directory" => $this->id
			);
			if($stmt->execute($params)) {
				//
				// Adding found comics to the list.
				foreach($stmt->fetchAll() as $row) {
					$this->_comics->addId($row["cmc_id"]);
				}
			}
			// @}
		}
	}
	/**
	 * This is an internal method called by the pseudo-method __set()
	 *
	 * @param string $name Name of the parameter being modified.
	 * @param string $value Value to set to the property with name $name.
	 * @return boolean Returns true when it is alright to modify the property.
	 */
	protected function subSet($name, &$value) {
		//
		// Loading global pointers.
		global $wcPageCache;
		//
		// If there's a cache manager active, this directory cache must
		// set as invalid because it's changing.
		if($wcPageCache) {
			$wcPageCache->invalidateByDirectory($this);
		}
		//
		// If name is scheduled to be change it must be re-indexed by the
		// search engine.
		if($name == "next_name") {
			$this->indexed = false;
			//
			// If the new name is that same than the old one, the
			// scheduled operation for renaming must be removed.
			if($value == $this->name) {
				$value = "";
			}
		}
		//
		// There are no conditions in this method that my interrupt the
		// property change.
		return true;
	}
	//
	// Public class methods.
	/**
	 * This is a sorting method based on comic id and it can be used on usort
	 * and uasort functions.
	 *
	 * @param WCDirectory $a Left operand.
	 * @param WCDirectory $b Right operand.
	 * @return boolean Returns true when the id of $a is greater than $b's.
	 */
	public static function IdSort(WCDirectory &$a, WCDirectory &$b) {
		return $a->id > $b->id;
	}
	/**
	 * This is a sorting method based on comic names and it can be used on
	 * usort and uasort functions.
	 *
	 * @param WCDirectory $a Left operand.
	 * @param WCDirectory $b Right operand.
	 * @return boolean Returns true when the name of $a is greater than
	 * $b's.
	 */
	public static function NameSort(WCDirectory &$a, WCDirectory &$b) {
		return $a->name > $b->name;
	}
}
?>